use std::{
    fs::File,
    io::{self, BufRead},
    path::{Path, PathBuf},
    sync::Arc,
};

use cudarc::driver::{CudaSlice, CudaStream, DriverError};
use glam::{Vec2, Vec3, Vec4};

use super::tgaimage::TGAImage;

#[derive(Clone)]
pub struct Model {
    verts: Vec<Vec4>,
    norms: Vec<Vec4>,
    tex: Vec<Vec2>,
    facet_vrt: Vec<usize>,
    facet_nrm: Vec<usize>,
    facet_tex: Vec<usize>,
    diffuse_map: TGAImage,
    normal_map: TGAImage,
    specular_map: TGAImage,
}

impl Model {
    pub fn from_file<P: AsRef<Path>>(path: &P) -> io::Result<Self> {
        let f = File::open(path)?;

        let mut verts = vec![];
        let mut norms = vec![];
        let mut tex = vec![];
        let mut facet_vrt = vec![];
        let mut facet_nrm = vec![];
        let mut facet_tex = vec![];

        let reader = io::BufReader::new(f);
        for line in reader.lines() {
            let line = line?;
            let mut comps = line.split_whitespace();

            if let Some(head) = comps.next() {
                match head {
                    "v" => verts.push(
                        Vec3::from_slice(
                            &comps
                                .take(3)
                                .map(|s| {
                                    s.parse::<f32>()
                                        .unwrap_or_else(|_| panic!("failed to parse {line}"))
                                })
                                .collect::<Vec<_>>(),
                        )
                        .extend(1.),
                    ),
                    "vn" => norms.push(
                        Vec3::from_slice(
                            &comps
                                .take(3)
                                .map(|s| {
                                    s.parse::<f32>()
                                        .unwrap_or_else(|_| panic!("failed to parse {line}"))
                                })
                                .collect::<Vec<_>>(),
                        )
                        .extend(0.)
                        .normalize(),
                    ),
                    "vt" => tex.push(
                        Vec2::Y
                            + Vec2::new(1., -1.)
                                * Vec2::from_slice(
                                    &comps
                                        .take(2)
                                        .map(|s| {
                                            s.parse::<f32>().unwrap_or_else(|_| {
                                                panic!("failed to parse {line}")
                                            })
                                        })
                                        .collect::<Vec<_>>(),
                                ),
                    ),
                    "f" => {
                        let v_t_n_arr = comps.take(3).collect::<Vec<_>>();
                        assert!(
                            v_t_n_arr.len() == 3,
                            "Error: the obj file is supposed to be triangulated"
                        );
                        for v_t_n in v_t_n_arr {
                            let v_t_n = v_t_n
                                .split("/")
                                .map(|s| {
                                    s.parse::<usize>()
                                        .unwrap_or_else(|_| panic!("failed to parse {s}"))
                                })
                                .collect::<Vec<usize>>();
                            assert!(v_t_n.len() == 3, "Error: v t n length not correct");
                            facet_vrt.push(v_t_n[0] - 1);
                            facet_tex.push(v_t_n[1] - 1);
                            facet_nrm.push(v_t_n[2] - 1);
                        }
                    }
                    _ => {}
                }
            }
        }

        let load_texture = |suffix: &str| -> io::Result<TGAImage> {
            let filename = path.as_ref().file_stem();
            let parent = if let Some(s) = path.as_ref().parent() {
                s
            } else {
                &PathBuf::from(&"")
            };
            let filename = parent.join(filename.unwrap().to_str().unwrap().to_string() + suffix);
            assert!(filename.exists(), "path {filename:?} not exist");
            TGAImage::from_tga_file(&filename)
        };

        Ok(Self {
            verts,
            norms,
            tex,
            facet_vrt,
            facet_nrm,
            facet_tex,
            diffuse_map: load_texture("_diffuse.tga")?,
            normal_map: load_texture("_nm.tga")?,
            specular_map: load_texture("_spec.tga")?,
        })
    }

    pub fn nverts(&self) -> usize {
        self.verts.len()
    }

    pub fn nfaces(&self) -> usize {
        self.facet_vrt.len() / 3
    }

    pub fn verts(&self) -> &[Vec4] {
        &self.verts
    }

    pub fn norms(&self) -> &[Vec4] {
        &self.norms
    }

    pub fn face_vertex(&self, iface: usize, nthvert: usize) -> Vec4 {
        self.verts()[self.facet_vrt[iface * 3 + nthvert]]
    }

    pub fn face_normal(&self, iface: usize, nthvert: usize) -> Vec4 {
        self.norms[self.facet_nrm[iface * 3 + nthvert]]
    }

    pub fn face_uv(&self, iface: usize, nthvert: usize) -> Vec2 {
        self.tex[self.facet_tex[iface * 3 + nthvert]]
    }

    pub fn normal_map(&self) -> &TGAImage {
        &self.normal_map
    }

    pub fn diffuse_map(&self) -> &TGAImage {
        &self.diffuse_map
    }

    pub fn specular_map(&self) -> &TGAImage {
        &self.specular_map
    }
}

pub struct GpuModel {
    pub verts: CudaSlice<f32>,
    pub verts_camera: CudaSlice<f32>,
    pub verts_screen: CudaSlice<f32>,
    pub facet_vrt: CudaSlice<u32>,
    pub norms: CudaSlice<f32>,
    pub norms_camera: CudaSlice<f32>,
    pub tex: CudaSlice<f32>,
    pub facet_tex: CudaSlice<u32>,
    pub normal_map: CudaSlice<u8>,
    pub normal_map_size: (usize, usize),
    pub diffuse_map: CudaSlice<u8>,
    pub diffuse_map_size: (usize, usize),
}

impl GpuModel {
    pub fn from_model(cuda_stream: &Arc<CudaStream>, model: &Model) -> Result<Self, DriverError> {
        let verts = model
            .verts()
            .iter()
            .map(|v| (*v).to_array())
            .collect::<Vec<_>>()
            .concat();
        let verts = cuda_stream.memcpy_stod(&verts)?;
        let facet_vrt = cuda_stream.memcpy_stod(
            &model
                .facet_vrt
                .iter()
                .map(|x| *x as u32)
                .collect::<Vec<_>>(),
        )?;

        let norms = model
            .norms()
            .iter()
            .map(|v| (*v).to_array())
            .collect::<Vec<_>>()
            .concat();
        let norms = cuda_stream.memcpy_stod(&norms)?;

        let tex = model
            .tex
            .iter()
            .map(|v| (*v).to_array())
            .collect::<Vec<_>>()
            .concat();
        let tex = cuda_stream.memcpy_stod(&tex)?;
        let facet_tex = cuda_stream.memcpy_stod(
            &model
                .facet_tex
                .iter()
                .map(|x| *x as u32)
                .collect::<Vec<_>>(),
        )?;

        let normal_map = cuda_stream.memcpy_stod(&model.normal_map().data)?;
        let diffuse_map = cuda_stream.memcpy_stod(&model.diffuse_map().data)?;

        Ok(GpuModel {
            verts_camera: verts.clone(),
            verts_screen: verts.clone(),
            verts,
            facet_vrt,
            norms_camera: norms.clone(),
            norms,
            tex,
            facet_tex,
            normal_map,
            normal_map_size: (model.normal_map().width(), model.normal_map().height()),
            diffuse_map,
            diffuse_map_size: (model.diffuse_map().width(), model.diffuse_map().height()),
        })
    }
}
